<?php

namespace Abo\Larasearch\V0\ElasticSearch;

use Abo\Generalutil\V1\Utils\ConfigUtil;
use Abo\Generalutil\V1\Utils\LogUtil;
use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;
use Illuminate\Support\Collection;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use RuntimeException;
use stdClass;

/**
 * Class Builder
 * Description: ElasticSearch 查询构造器
 * @package Abo\Larasearch\ElasticSearch
 */
class EsBuilder
{
    /** @var \Elasticsearch\Client|null */
    protected $elastisearch = null;

    /** @var Grammar|null */
    protected $grammar = null;

    public $wheres = [];
    public $columns = [];
    public $offset = null;
    public $limit = null;
    public $orders = [];
    public $aggs = [];

    public $index = '';
    public $type = '';
    public $scroll = '';
    protected $config = [];

    public $operators = [
        '=' => 'eq',
        '>' => 'gt',
        '>=' => 'gte',
        '<' => 'lt',
        '<=' => 'lte',
    ];

    protected $queryLogs = [];
    protected $enableQueryLog = false;

    /**
     * EsBuilder constructor.
     */
    public function __construct( array $config, Grammar $grammar, Client $client )
    {
        $this->config = $config;
        $this->setGrammar( $grammar );
        $this->setElasticSearch( $client );
        $this->setDefault();
    }

    /** 设置config 默认索引/类型 @return void */
    protected function setDefault()
    {
        if ( !empty( $this->config['index'] ) ) {
            $this->index = $this->config['index'];
        }

        if ( !empty( $this->config[ 'type' ] ) ) {
            $this->type = $this->config[ 'type' ];
        }
    }

    /** 指定索引 @return EsBuilder */
    public function index( string $index = '' ): self
    {
        $this->index = $index;

        return $this;
    }

    /** 指定类型 @return EsBuilder */
    public function type( $type ): self
    {
        $this->type = $type;

        return $this;
    }

    /* 设置DSL构造器 @return $this */
    private function setGrammar( Grammar $grammar )
    {
        $this->grammar = $grammar;

        return $this;
    }

    /** 返回DSL构造器 @return Grammar|null */
    private function getGrammar()
    {
        return $this->grammar;
    }

    /** @param Client $client @return $this */
    private function setElasticSearch( Client $client )
    {
        $this->elastisearch = $client;

        return $this;
    }

    /** @return \Elasticsearch\Client|null */
    public function getElasticSearch()
    {
        return $this->elastisearch;
    }

    /** @return \Elasticsearch\Client|null */
    public static function getElasticSearchInstance():self 
    {
        // $config = include_once ( base_path( 'config/larasearch.php' ) );
        $ConfigUtil = new ConfigUtil( base_path( 'config/larasearch.php' ) );
        $elasticsearch = $ConfigUtil->get( 'elasticsearch', 1 );

        LogUtil::debug( 'setConnectionPool', json_encode( $elasticsearch ) );

        $clientBuilder = ClientBuilder::create()
            ->setConnectionPool( $elasticsearch[ 'connection_pool' ] )
            ->setSelector( $elasticsearch[ 'selector' ] )
            ->setHosts( $elasticsearch[ 'hosts' ] );

        if ( $elasticsearch[ 'open_log' ] ) {
            $logger = new Logger( 'elasticsearch' );
            $logger->pushHandler( new StreamHandler( $elasticsearch[ 'log_path' ], Logger::INFO ) );

            $clientBuilder = $clientBuilder->setLogger( $logger );
        }

        return new EsBuilder(
            $elasticsearch,
            new Grammar(),
            $clientBuilder->build()
        );
    }

    /** @param array $params | string $method @return mixed */
    protected function runQuery( array $params, string $method = 'search' )
    {
        if ($this->enableQueryLog) {
            $this->queryLogs[] = $params;
        }

        $ConfigUtil = new ConfigUtil( base_path( 'config/larasearch.php' ) );
        $config = $ConfigUtil->get( 'elasticsearch' );
        if ( $config['open_log'] && is_array( $params )) {
            LogUtil::info( ' elasticsearch.INFO: ', $params, $config['log_path'] );
        }
        return call_user_func( [ $this->elastisearch, $method ], $params );
    }

    /** @return static */
    public function newQuery(): self
    {
        return new static( $this->config, $this->grammar, $this->elastisearch );
    }

/* ======================  日志 V  ====================== */

    /** @return EsBuilder */
    public function enableQueryLog(): self
    {
        $this->enableQueryLog = true;

        return $this;
    }

    /* @return EsBuilder */
    public function disableQueryLog(): self
    {
        $this->enableQueryLog = false;

        return $this;
    }

    public function getQueryLog(): array
    {
        return $this->queryLogs;
    }

    public function getLastQueryLog()
    {
        return empty( $this->queryLogs ) ? '' : end( $this->queryLogs );
    }

/* ======================  分页 V  ====================== */
    /** @param int $value @return EsBuilder */
    public function limit( int $value ): self
    {
        $this->limit = $value;
        return $this;
    }

    /** @param int $value @return EsBuilder */
    public function offset( int $value ): self
    {
        $this->offset = $value;
        return $this;
    }

    /**
     * @param int $page
     * @param int $perPage
     * @return Collection
     */
    public function paginate( int $page, int $perPage = 15 ): Collection
    {
        $from = ( ( $page * $perPage ) - $perPage );

        if ( empty( $this->offset ) ) {
            $this->offset = $from;
        }

        if ( empty( $this->limit ) ) {
            $this->limit = $perPage;
        }

        $results = $this->runQuery( $this->grammar->compileSelect( $this ) );

        $data = collect( $results[ 'hits' ][ 'hits' ])->map( function ( $hit ) {
            return ( object ) array_merge( $hit[ '_source' ], [ '_id' => $hit[ '_id' ] ] );
        });

        $maxPage = intval( ceil( $results[ 'hits' ][ 'total' ] / $perPage ) );
        return collect([
            'total' => $results['hits']['total'],
            'per_page' => $perPage,
            'current_page' => $page,
            'next_page' => $page < $maxPage ? $page + 1 : $maxPage,
            'total_pages' => $maxPage,
            'from' => $from,
            'to' => $from + $perPage,
            'data' => $data
        ]);
    }

/* ======================  排序 V  ====================== */

    /**
     * @param string $field
     * @param $sort asc | desc
     * @return EsBuilder
     */
    public function orderBy( string $field, $sort ): self
    {
        $this->orders[ $field ] = $sort;

        return $this;
    }

    /**
     * @param $field
     * @param $type
     * @return EsBuilder
     */
    public function aggBy( $field, $type ): self
    {
        is_array( $field ) ?
            $this->aggs = $field :
            $this->aggs[ $field ] = $type;

        return $this;
    }

    /**
     * @param string $scroll 1m 1s
     * @return EsBuilder
     */
    public function scroll( string $scroll ): self
    {
        $this->scroll = $scroll;

        return $this;
    }

    /**
     * @param $columns
     * @return EsBuilder
     */
    public function select( $columns ): self
    {
        $this->columns = is_array($columns) ? $columns : func_get_args();

        return $this;
    }

/* ======================  条件 V  ====================== */

    /** match_phrase 词语查询 不分词 */
    public function whereMatchPhrase( string $field, $value ): self
    {
        return $this->where( $field, '=', $value, 'match_phrase', 'and' );
    }

    public function orWhereMatchPhrase( string $field, $value ): self
    {
        return $this->where( $field, '=', $value, 'match_phrase', 'or' );
    }

    /**
     * @param $field
     * @param $value
     * @param string $boolean
     * @return EsBuilder
     */
    public function whereMatch( string $field, $value ): self
    {
        return $this->where( $field, '=', $value, 'match', 'and' );
    }

    /**
     * @param $field
     * @param $value
     * @param string $boolean
     * @return EsBuilder
     */
    public function orWhereMatch( string $field, $value ): self
    {
        return $this->whereMatch($field, $value, 'or');
    }

    /**
     * @param $field
     * @param $value
     * @param string $boolean
     * @return EsBuilder
     */
    public function whereTerm( string $field, $value ): self
    {
        return $this->where($field, '=', $value, 'term', 'and' );
    }

    /**
     * @param $field
     * @param $value
     * @param string $boolean
     * @return EsBuilder
     */
    public function orWhereTerm( string $field, $value ): self
    {
        return $this->where($field, '=', $value, 'term', 'or' );
    }

    /**
     * @param $field
     * @param $value
     * @return EsBuilder
     */
    public function whereMultiMatch( array $field, $value  )
    {
        return $this->where( $field, '=', $value, 'multi_match', 'and' );
    }

    /**
     * @param $field
     * @param $value
     * @return EsBuilder
     */
    public function orWhereMultiMatch( array $field, $value  )
    {
        return $this->where( $field, '=', $value, 'multi_match', 'or' );
    }

    /**
     * @param $field
     * @param array $value
     * @return EsBuilder
     */
    public function whereIn( string $field, array $value)
    {
        return $this->where( function (EsBuilder $query) use ($field, $value) {
            array_map(function ($item) use ($query, $field) {
                $query->orWhereTerm($field, $item);
            }, $value );
        });
    }

    /**
     * @param $field
     * @param array $value
     * @return EsBuilder
     */
    public function orWhereIn( string $field, array $value)
    {
        return $this->orWhere(function (EsBuilder $query) use ($field, $value) {
            array_map(function ($item) use ($query, $field) {
                $query->orWhereTerm($field, $item);
            }, $value);
        });
    }

    /**
     * @param $field
     * @param null $operator
     * @param null $value
     * @param string $boolean
     * @return EsBuilder
     */
    public function whereRange( string $field, $operator = null, $value = null ): self
    {
        return $this->where($field, $operator, $value, 'range', 'and');
    }

    /**
     * @param $field
     * @param null $operator
     * @param null $value
     * @return EsBuilder
     */
    public function orWhereRange( string $field, $operator = null, $value = null): self
    {
        return $this->where($field, $operator, $value, 'or');
    }

    /**
     * @param $field
     * @param string $value
     * @param string $boolean
     * @param array $addition [
        'fuzziness' 字符串 允许差异程度(长度): 默认自动
        'prefix_length' 查询 匹配词 最小长度: 默认无设置
        'max_expansions' ?
     ]
     * @return EsBuilder
     */
    public function whereLike( string $field, string $value, array $addition): self
    {
        return $this->where($field, null, $value, 'like', 'and', $addition);
    }

    /**
     * @param $field
     * @param array $values
     * @param string $boolean
     * @return EsBuilder
     */
    public function orWhereLike( string $field, string $value, array $addition = []): self
    {
        return $this->where($field, null, $value, 'like', 'or', $addition);
    }

    /**
     * @param $field
     * @param array $values
     * @param string $boolean
     * @return EsBuilder
     */
    public function whereBetween( string $field, array $values ): self
    {
        return $this->where( $field, null, $values, 'range', 'and' );
    }

    /**
     * @param $field
     * @param array $values
     * @return EsBuilder
     */
    public function orWhereBetween( string $field, array $values ): self
    {
        return $this->where( $field, null, $values, 'range', 'or' );
    }

    /**
     * @param $column
     * @param null $operator
     * @param null $value
     * @param string $leaf
     * @param string $boolean
     * @return EsBuilder
     */
    public function where( $column, $operator = null, $value = null, $leaf = 'term', $boolean = 'and', array $addition = [] ): self
    {
        if ($column instanceof \Closure) {
            return $this->whereNested($column, $boolean);
        }

        if (func_num_args() === 2) {
            list($value, $operator) = [$operator, '='];
        }

        if (is_array($operator)) {
            list($value, $operator) = [$operator, null];
        }

        if ($operator !== '=') {
            switch ( $leaf ) {
                case 'like':
                    $leaf = 'like';
                    break;
                default:
                    $leaf = 'range';
                    break;
            }
        }

        if (is_array($value) && $leaf === 'range') {
            $value = [
                $this->operators['>='] => $value[0],
                $this->operators['<='] => $value[1],
            ];
        }

        $type = 'Basic';

        $operator = $operator ? $this->operators[$operator] : $operator;

        $this->wheres[] = compact(
            'type', 'column', 'leaf', 'value', 'boolean', 'operator', 'addition'
        );

        return $this;
    }

    /**
     * @param $field
     * @param null $operator
     * @param null $value
     * @param string $leaf
     * @return EsBuilder
     */
    public function orWhere( $field, $operator = null, $value = null, $leaf = 'term'): self
    {
        if ( 2 === func_num_args() ) {
            list( $value, $operator ) = [ $operator, '=' ];
        }

        return $this->where( $field, $operator, $value, $leaf, 'or' );
    }

    /**
     * @param \Closure $callback
     * @param $boolean
     * @return EsBuilder
     */
    public function whereNested( \Closure $callback, $boolean ): self
    {
        $query = $this->newQuery();

        call_user_func( $callback, $query );

        return $this->addNestedWhereQuery( $query, $boolean );
    }

    /**
     * @param $query
     * @param string $boolean
     * @return EsBuilder
     */
    protected function addNestedWhereQuery( $query, $boolean = 'and' ): self
    {
        if ( count( $query->wheres ) ) {
            $type = 'Nested';
            $this->wheres[] = compact( 'type', 'query', 'boolean' );
        }

        return $this;
    }

/* ======================  结果 V  ====================== */

    /** @return stdClass|null */
    public function first()
    {
        $this->limit = 1;

        $results = $this->runQuery( $this->grammar->compileSelect( $this ) );

        return $this->metaData( $results )->first();
    }

    /**
     * @return Collection
     */
    public function get(): Collection
    {
        $results = $this->runQuery( $this->grammar->compileSelect( $this ) );

        return $this->metaData( $results );
    }

    /**
     * @param $id
     * @return null|object
     */
    public function byId( $id )
    {
        //$query = $this->newQuery();

        $result = $this->runQuery(
            $this->whereTerm( '_id', $id )->getGrammar()->compileSelect( $this )
        );

        return isset($result['hits']['hits'][0]) ?
            $this->sourceToObject($result['hits']['hits'][0]) :
            null;
    }

    /**
     * @param $id
     * @return stdClass
     */
    public function byIdOrFail($id): stdClass
    {
        $result = $this->byId($id);

        if (empty($result)) {
            throw new RuntimeException('Resource not found');
        }

        return $result;
    }

    /**
     * @param callable $callback
     * @param int $limit
     * @param string $scroll
     * @return bool
     */
    public function chunk(callable $callback, $limit = 2000, $scroll = '10m')
    {
        if (empty($this->scroll)) {
            $this->scroll = $scroll;
        }

        if (empty($this->limit)) {
            $this->limit = $limit;
        }

        $results = $this->runQuery($this->grammar->compileSelect($this), 'search');

        if ($results['hits']['total'] === 0) {
            return null;
        }

        $total = $this->limit;
        $whileNum = intval(floor($results['hits']['total'] / $this->limit));

        do {
            if (call_user_func($callback, $this->metaData($results)) === false) {
                return false;
            }

            $results = $this->runQuery(['scroll_id' => $results['_scroll_id'], 'scroll' => $this->scroll], 'scroll');

            $total += count($results['hits']['hits']);
        } while ($whileNum--);
    }

    /** @return int */
    public function count(): int
    {
        $result = $this->runQuery($this->grammar->compileSelect($this), 'count');
        return $result['count'];
    }

/* ======================  索引 V  ====================== */

    /**
     * @param array $data
     * @param null $id
     * @param string $key
     * @return stdClass
     */
    public function indexCreate(array $data, $key = 'id', $id = null): stdClass
    {
        $id = $id ? $id : isset( $data[ $key ] ) ? $data[$key] : uniqid();

        $result = $this->runQuery(
            $this->grammar->compileCreate($this, $id, $data),
            'create'
        );

        if (!isset($result['result']) || $result['result'] !== 'created') {
            throw new RunTimeException('Create params: ' . json_encode($this->getLastQueryLog()));
        }

        $data['_id'] = $id;
        return (object)$data;
    }

/* ======================  文档 V  ====================== */

    public function docInsert( array $data, $key = 'id', $id = null): stdClass
    {
        $id = $id ? $id : isset( $data[ $key ] ) ? $data[$key] : uniqid();

        $result = $this->runQuery(
            $this->grammar->compileDocInsert($this, $id, $data),
            'index'
        );

        if (!isset($result['result']) || !$result['result']) { //  !== 'created' / 'updated' 有可能存在而进行更新
            throw new RunTimeException('Create params: ' . json_encode($this->getLastQueryLog()));
        }

        $data['_id'] = $id;
        return (object)$data;
    }

    public function docBulk( array $data, string $key = 'id' ): stdClass
    {
        $params = $this->grammar->compileDocBulk($this, $data, $key);

        $result = $this->runQuery(
            $params,
            'bulk'
        );

        if (!isset( $result[ 'errors' ] ) || $result[ 'errors' ]) {
            throw new RunTimeException('Bulk params: ' . json_encode($this->getLastQueryLog()));
        }

        return (object)$data;
    }

    public function docUpdate($id, array $data): bool
    {
        $result = $this->runQuery(
            $this->grammar->compileUpdate($this, $id, $data),
            'update'
        );

        if (!isset($result['result']) || !$result['result'] ) { //  !== 'updated' / 'created'
            throw new RunTimeException('Update error params: ' . json_encode($this->getLastQueryLog()));
        }

        return true;
    }
    
    public function docDelete($id)
    {
        $result = $this->runQuery(
            $this->grammar->compileDelete($this, $id),
            'delete'
        );

        if (!isset($result['result']) || $result['result'] !== 'deleted') {
            throw new RunTimeException('Delete error params:' . json_encode($this->getLastQueryLog()));
        }

        return true;
    }

    /** 结果转换 Collection @return Collection */
    protected function metaData(array $results): Collection
    {
        return collect($results['hits']['hits'])->map(function ($hit) {
            return $this->sourceToObject($hit);
        });
    }

    /** 结果转换 object @return object */
    protected function sourceToObject( array $result ): stdClass
    {
        return (object)array_merge($result['_source'], ['_id' => $result['_id']]);
    }
}